梦想不会自己发光,真正闪耀的是那个为梦狂奔的你。献给知行的孩子们!(Eric.He著)
并查集(又称不相交集合 Disjoint Set Union,DSU)是一种高效处理动态连通性问题的树形数据结构,核心支持查找元素所属集合和合并两个不相交集合操作,经过路径压缩和按秩 / 大小合并优化后,操作均摊时间复杂度接近 O(1),是图论、贪心算法等领域的常用工具, 广泛应用于图的连通性判断、最小生成树(Kruskal 算法)、分组问题等场景。本文将详细讲解并查集的原理与实现,内容由浅入深,所有代码可直接编译运行。
图结构的实现方式:
学习经典应用场景前,请根据上面的教程封装好自定义图,所有场景实例直接复用
用树形结构表示每个集合,每个集合有一个根节点(代表元),集合内所有元素最终都指向根节点:
并查集的原始实现会出现树退化成链表的情况,需通过两种优化将时间复杂度降至均摊 O(α(n)),α(n) 为阿克曼函数的反函数,增长极慢,n<10600 时 α(n)≤5,可认为是常数)。
并查集的实现围绕父节点数组和秩数组展开,先定义两个基础数组:
找到元素 x 所属集合的根节点,同时压缩查找路径。
//递归版(简洁,适合理解,数据量大时可能栈溢出)
int find(int x) {
// 递归终止:根节点的父节点是自己
if (parent[x] != x) {
// 路径压缩:将x的父节点直接指向根节点
parent[x] = find(parent[x]);
}
return parent[x];
}
//迭代版(推荐,无栈溢出风险,效率更高):
int find(int x) {
// 先找到根节点
int root = x;
while (parent[root] != root) {
root = parent[root];
}
// 路径压缩:将查找路径上所有节点直接指向根节点
while (parent[x] != root) {
int next = parent[x]; // 保存x的原父节点
parent[x] = root; // x直接指向根节点
x = next; // 继续处理原父节点
}
return root;
}
将元素 x 和 y 所在的两个集合合并为一个集合。
void unite(int x, int y) {
x = find(x); // 找到x的根
y = find(y); // 找到y的根
if (x == y) return; // 同一集合,直接返回
// 按秩合并:小秩树指向大秩树
if (rank[x] < rank[y]) {
parent[x] = y;
} else {
parent[y] = x;
// 秩相等时,合并后根节点秩+1
if (rank[x] == rank[y]) {
rank[x]++;
}
}
}
判断元素 x 和 y 是否属于同一个集合(即是否连通)。
bool same(int x, int y) {
return find(x) == find(y);
}
通过两次 DFS 实现强连通分量的查找。
算法解析:
#include <iostream>
#include <vector>
#include"ArrayGraph.h"
using namespace std;
vector<int> parent; // 父节点数组
vector<int> ranks; // 秩数组(树的高度)
// 查找:带路径压缩
int find(int x) {
// 先找到根节点
int root = x;
while (parent[root] != root) {
root = parent[root];
}
// 路径压缩:将查找路径上所有节点直接指向根节点
while (parent[x] != root) {
int next = parent[x]; // 保存x的原父节点
parent[x] = root; // x直接指向根节点
x = next; // 继续处理原父节点
}
return root;
}
// 合并:按秩合并
void unite(int x, int y) {
x = find(x); // 找到x的根
y = find(y); // 找到y的根
if (x == y) return; // 同一集合,直接返回
// 按秩合并:小秩树指向大秩树
if (ranks[x] < ranks[y]) {
parent[x] = y;
} else {
parent[y] = x;
// 秩相等时,合并后根节点秩+1
if (ranks[x] == ranks[y]) {
ranks[x]++;
}
}
}
// 判断是否连通:同一集合返回true
bool same(int x, int y) {
return find(x) == find(y);
}
// 测试案例
int main() {
ArrayGraph graph;
// 1. 添加顶点(A、B、C、D、E、F、G、H、I)
graph.addVertex('A');
graph.addVertex('B');
graph.addVertex('C');
graph.addVertex('D');
graph.addVertex('E');
graph.addVertex('F');
graph.addVertex('G');
graph.addVertex('H');
graph.addVertex('I');
// 2. 添加边(有权无向图)
graph.addEdge('A','B',3);
graph.addEdge('A','C',1);
graph.addEdge('A','D',2);
graph.addEdge('B','A',3);
graph.addEdge('B','D',2);
graph.addEdge('C','A',1);
graph.addEdge('C','D',2);
graph.addEdge('C','F',2);
graph.addEdge('D','A',2);
graph.addEdge('D','B',2);
graph.addEdge('D','C',2);
graph.addEdge('D','E',2);
graph.addEdge('E','D',2);
graph.addEdge('E','F',4);
graph.addEdge('F','C',2);
graph.addEdge('F','E',4);
graph.addEdge('F','G',3);
graph.addEdge('G','F',3);
graph.addEdge('H','I',3);
graph.addEdge('I','H',3);
// 3. 打印邻接矩阵
graph.printAdjacency();
// 4. 初始化
parent.resize(graph.vertexNum);
ranks.resize(graph.vertexNum, 1); // 初始秩均为1
for (int i = 0; i < graph.vertexNum; i++) {
parent[i] = i; // 初始父节点为自身,自成集合
}
// 5. 合并操作
for(int i=0;i < graph.vertexNum;i++)
{
for(int j=0;j < graph.vertexNum;j++)
{
if(graph.graph[i][j]>NO_EDGE)
{
unite(i, j);
}
}
}
// 6. 判断连通性
char start='A';
char target='E';
int startIndex = graph.findVertexIndex(start);
int targetIndex = graph.findVertexIndex(target);
cout << "父节点:";
for(int i=0;i < graph.vertexNum;i++)
{
cout << parent[i] << " ";
}
cout << endl;
cout << " 秩:";
for(int i=0;i < graph.vertexNum;i++)
{
cout << ranks[i] << " ";
}
cout << endl;
cout << "A和E是否连通:" << (same(startIndex,targetIndex) ? "是" : "否") << endl; // 是
return 0;
}
========= 带权邻接矩阵 ========
A B C D E F G H I
A 0 3 1 2 0 0 0 0 0
B 3 0 0 2 0 0 0 0 0
C 1 0 0 2 0 2 0 0 0
D 2 2 2 0 2 0 0 0 0
E 0 0 0 2 0 4 0 0 0
F 0 0 2 0 4 0 3 0 0
G 0 0 0 0 0 3 0 0 0
H 0 0 0 0 0 0 0 0 3
I 0 0 0 0 0 0 0 3 0
========================
父节点:0 0 0 0 0 0 0 7 7
秩:2 1 1 1 1 1 1 2 1
A和E是否连通:是
并查集的核心是处理动态连通性,以下是最常见的应用: